package com.wzx.core.aop;

import com.wzx.core.aop.annotaction.*;
import com.wzx.core.aop.bean.PointcutProperty;
import com.wzx.core.aop.constant.AdviceType;
import com.wzx.core.aop.invoke.AspectLogic;
import com.wzx.core.ioc.anootaction.Transactional;
import com.wzx.core.ioc.bean.BeanDefinition;
import com.wzx.core.ioc.bean.ObjectFactory;
import com.wzx.core.ioc.context.AbstractApplicationContext;
import com.wzx.core.ioc.context.AnnotationApplicationContext;
import com.wzx.core.aop.invoke.TransactionInvocation;
import com.wzx.core.utils.ClassUtil;

import java.lang.reflect.Method;
import java.util.*;

public class AdviceClassRegister {

    private static final Map<String, Map<String, List<AspectLogic>>> PROXY_CLASS = new HashMap<>();
    private static boolean init = false;

    private AbstractApplicationContext context;

    public AdviceClassRegister(AbstractApplicationContext context) {
        this.context = context;
    }

    public boolean isAopProxy(String beanName, Class cls) {
        if (!init) {
            this.init();
            this.init = true;
        }

        if (isTransaction(cls)) {
            this.resolveTransactionClass(beanName, cls);
        }

        if (PROXY_CLASS.containsKey(cls.getName())) {
            return true;
        }

        return false;
    }

    private void resolveTransactionClass(String beanName, Class cls) {
        TransactionInvocation ti = new TransactionInvocation(context);
        ObjectFactory objectFactory = () -> ti;
        if (cls.isAnnotationPresent(Transactional.class)) {
            Method[] methods = cls.getDeclaredMethods();
            for (Method method : methods) {
                PointcutProperty pp = new PointcutProperty(cls.getName(), method.getName());
                this.createAspectLogicProxyClass(pp, objectFactory, ti.getAfter());
                this.createAspectLogicProxyClass(pp, objectFactory, ti.getBefore());
                this.createAspectLogicProxyClass(pp, objectFactory, ti.getRunning());
                this.createAspectLogicProxyClass(pp, objectFactory, ti.getThrowing());
            }
        } else {
            Method[] methods = cls.getDeclaredMethods();
            for (Method method : methods) {
                if (!method.isAnnotationPresent(Transactional.class)) {
                    continue;
                }
                PointcutProperty pp = new PointcutProperty(cls.getName(), method.getName());
                this.createAspectLogicProxyClass(pp, objectFactory, ti.getAfter());
                this.createAspectLogicProxyClass(pp, objectFactory, ti.getBefore());
                this.createAspectLogicProxyClass(pp, objectFactory, ti.getRunning());
                this.createAspectLogicProxyClass(pp, objectFactory, ti.getThrowing());
            }
        }
    }

    private void createAspectLogicProxyClass(PointcutProperty pp, ObjectFactory aspectObjectFactory, Method aspectMethod) {
        Map<String, List<AspectLogic>> advisorMap = Optional.ofNullable(PROXY_CLASS.get(pp.getClassName())).orElse(new HashMap<>());

        AdviceType type = this.getAdviceType(aspectMethod);

        String adviceKey = this.getAdviceKey(pp.getMethodName(), type);

        List<AspectLogic> aspectLogics = Optional.ofNullable(advisorMap.get(adviceKey)).orElse(new ArrayList<>());

        aspectLogics.add(this.createAspectLogic(aspectObjectFactory, aspectMethod));

        advisorMap.put(adviceKey, aspectLogics);
        PROXY_CLASS.put(pp.getClassName(), advisorMap);
    }


    private void init() {
        List<BeanDefinition> beanList = ((AnnotationApplicationContext) context).getNeedResolveComponentClassList();
        for (BeanDefinition beanDefinition : beanList) {
            if (!isComponent(beanDefinition)) {
                continue;
            }
            if (!isAspect(beanDefinition)) {
                continue;
            }

            Method[] methods = beanDefinition.getOriginClass().getDeclaredMethods();
            for (Method aspectMethod : methods) {
                if (isPointcut(aspectMethod)) {

                    PointcutProperty pp = this.getPointcutProperty(aspectMethod);

                    this.createAspectLogicProxyClass(pp, () -> context.getBean(beanDefinition.getBeanName()), aspectMethod);
                }
            }
        }
    }

    private boolean isTransaction(Class cls) {
        if (cls.isAnnotationPresent(Transactional.class)) {
            return true;
        }
        Method[] methods = cls.getDeclaredMethods();
        for (Method method : methods) {
            if (method.isAnnotationPresent(Transactional.class)) {
                return true;
            }
        }
        return false;
    }


    private PointcutProperty getPointcutProperty(Method aspectMethod) {
        String point = null;
        if (aspectMethod.isAnnotationPresent(Before.class)) {
            Before before = aspectMethod.getAnnotation(Before.class);
            point = before.value();
        }
        if (aspectMethod.isAnnotationPresent(After.class)) {
            After before = aspectMethod.getAnnotation(After.class);
            point = before.value();
        }
        if (aspectMethod.isAnnotationPresent(Running.class)) {
            Running before = aspectMethod.getAnnotation(Running.class);
            point = before.value();
        }
        if (aspectMethod.isAnnotationPresent(Throwing.class)) {
            Throwing before = aspectMethod.getAnnotation(Throwing.class);
            point = before.value();
        }
        int i = point.lastIndexOf('.');
        String className = point.substring(0, i);
        String methodName = point.substring(i + 1);
        return new PointcutProperty(className, methodName);
    }


    private AspectLogic createAspectLogic(ObjectFactory aspectObjectFactory, Method m) {
        AspectLogic aspectLogic = new AspectLogic(aspectObjectFactory, m);
        return aspectLogic;
    }

    private AdviceType getAdviceType(Method m) {
        if (m.isAnnotationPresent(Before.class)) {
            return AdviceType.BEFORE;
        }
        if (m.isAnnotationPresent(After.class)) {
            return AdviceType.AFTER;
        }
        if (m.isAnnotationPresent(Running.class)) {
            return AdviceType.RUNNING;
        }
        if (m.isAnnotationPresent(Throwing.class)) {
            return AdviceType.THROWING;
        }
        return null;
    }

    private boolean isPointcut(Method m) {
        List<Class> list = Arrays.asList(Before.class, After.class, Running.class, Throwing.class);
        for (Class cls : list) {
            if (m.isAnnotationPresent(cls)) {
                return true;
            }
        }
        return false;
    }

    private boolean isAspect(BeanDefinition beanDefinition) {
        return beanDefinition.getOriginClass().isAnnotationPresent(Aspect.class);
    }

    private boolean isComponent(BeanDefinition beanDefinition) {
        return ClassUtil.isComponent(beanDefinition.getOriginClass());
    }


    private String getAdviceKey(String methodName, AdviceType before) {
        return methodName + '.' + before.name();
    }

    public List<AspectLogic> createAspectLogic(String className, String methodName, AdviceType before) {
        if (PROXY_CLASS.containsKey(className)) {
            Map<String, List<AspectLogic>> adviceMap = PROXY_CLASS.get(className);
            String key = this.getAdviceKey(methodName, before);
            if (adviceMap.containsKey(key)) {
                return adviceMap.get(key);
            }
        }
        return null;
    }
}
